package se.sweco.gpsservice;

import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TooManyListenersException;

import se.sweco.gpsservice.datatypes.*;

public class GPSConnect {
	GPSListener listener;
	GPSUpdateListener updateListener;
	CommPortIdentifier portId;
	SerialPort sPort = null;
	Timer simTimer = null;
	SimDataTask simTask = null;
	
	GPSDataGPGGA GPGGA;
	GPSDataGPGLL GPGLL;
	GPSDataGPGSA GPGSA;
	GPSDataGPRMC GPRMC;
	GPSDataGPVTG GPVTG;
	
	private double latitude = 0.0f;
	private String latitudeType = "";
	private double longitude = 0.0f;
	private String longitudeType = "";
	private double speedKilometer = 0.0f;
	private int numberOfSatellites = 0;
	
	//For converstion ....
	private static final double flattening = (1.0 / 298.257222101);
	private static final double axis = 6378137.0;
	private static final double central_meridian = 15.0 + 48.0 / 60.0 + 22.624306 / 3600.0;
	private static final double false_northing = -667.711;
	private static final double false_easting = 1500064.274;
	private static final double scale = 1.00000561024;
	
	private static final double e2 = flattening * (2.0 - flattening);
	private static final double n = flattening / (2.0 - flattening);
	private static final double a_roof = axis / (1.0 + n) * (1.0 + n * n / 4.0 + n * n * n * n / 64.0);
	private static final double delta1 = n / 2.0 - 2.0 * n * n / 3.0 + 37.0 * n * n * n / 96.0 - n * n * n * n / 360.0;
	private static final double delta2 = n * n / 48.0 + n * n * n / 15.0 - 437.0 * n * n * n * n / 1440.0;
	private static final double delta3 = 17.0 * n * n * n / 480.0 - 37 * n * n * n * n / 840.0;
	private static final double delta4 = 4397.0 * n * n * n * n / 161280.0;
	private static final double Astar = e2 + e2 * e2 + e2 * e2 * e2 + e2 * e2 * e2 * e2;
	private static final double Bstar = -(7.0 * e2 * e2 + 17.0 * e2 * e2 * e2 + 30.0 * e2 * e2 * e2 * e2) / 6.0;
	private static final double Cstar = (224.0 * e2 * e2 * e2 + 889.0 * e2 * e2 * e2 * e2) / 120.0;
	private static final double Dstar = -(4279.0 * e2 * e2 * e2 * e2) / 1260.0;
	
	private static final double deg_to_rad = Math.PI / 180;
	private static final double lambda_zero = central_meridian * deg_to_rad;
	
	//Internal task for updateing gps data from file.. 
	class SimDataTask extends TimerTask {
		public BufferedReader file = null;
		GPSConnect parent = null;
		String filename = "";
		String vehicleId = "";
		
		int count = 0;
		public SimDataTask(String filename, String vehicleId, GPSConnect parent) throws IOException {
			file = new BufferedReader(new FileReader(filename));
			this.parent = parent;
			this.filename = filename;
			parent.setNumberOfSatellites(-1);
			file.readLine();
			this.vehicleId = vehicleId;
		}
		
		public void run() {
			try {
				int oldCount = count;
				String[] dataList = null;
				while(count == oldCount) {
					String data = file.readLine();
					if(data == null) {
						if(count == 0) {
							throw new IOException("Missing data..");
						}
						
						file = new BufferedReader(new FileReader(filename));
						data = file.readLine();
					}
					
					dataList = data.split(";");
					if(vehicleId.equals(dataList[0])) {
						count++;
					}
				}
				
				String x = dataList[2];
				String y = dataList[3];
				
				double latlon[] = parent.convertRT90ToWGS84(new Double(x), new Double(y));
				
				parent.setLatitude(latlon[0]);
				parent.setLongitude(latlon[1]);
				
				if(updateListener != null) {
					updateListener.GPSUpdateEvent(new GPSUpdateEvent(parent, GPSUpdateEvent.LOCATION_UPDATE));
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	GPSConnect() {
	}
	
	public void cleanUp() {
		if(this.sPort != null) {
			this.sPort.close();
			this.sPort = null;
		}
		
		if(simTimer != null) {
			simTimer.cancel();
			if(simTask != null) {
				try {
					simTask.file.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			simTimer = null;
		}
	}
	
	/*
	 * Intialize the GPS unit on the given port, with the given baudrate 
	 * (ex: "COM11",38400). This method might throw a NoSuchPortException 
	 * if we are trying to open an port that dosent exist, if the port 
	 * is in use, or no device exist on that port, false is returned. 
	 * Works with all gps units that output NMEA messages (http://www.gpsinformation.org/dale/nmea.htm#GGA) 
	 * 
	 * @param comPort this specifies what comport to connect on
	 * @param comPort this specifies what baudrate to use
	 */
	public boolean connect(String comPort, int baudrate) throws NoSuchPortException {
		if(sPort == null) {
			System.out.println("Opening port: " + comPort + " baudrate: " + baudrate);
			portId = CommPortIdentifier.getPortIdentifier(comPort);
			try {
				sPort = (SerialPort) portId.open("gpsconnecter", 1000);
				sPort.setSerialPortParams(baudrate,SerialPort.DATABITS_8, SerialPort.STOPBITS_1,SerialPort.PARITY_NONE);
				sPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
				listener = new GPSListener(sPort.getInputStream(), this);
				sPort.addEventListener(listener);
				sPort.enableReceiveTimeout(1);
				sPort.notifyOnDataAvailable(true);
			} catch (PortInUseException e) {
				e.printStackTrace();
				return false;
			} catch (UnsupportedCommOperationException e) {
				e.printStackTrace();
				return false;
			} catch (IOException e) {
				e.printStackTrace();
				return false;
			} catch (TooManyListenersException e) {
				e.printStackTrace();
				return false;
			}
		}
		return true;
	}
	
	/*
	 * This loads a file from the filesystem and simulates gps data.. 
	 * 
	 * @param filename The filename of the file containg the gps data
	 * @param vehicleId The id of the vehicle of wich the data is to be taken..
	 * @param updateFrequenzy At what frequeny shall the data be updated.. 
	 */
	public boolean simulateData(String filename, String vehicleId, int updateFrequenzy) {
		if(simTimer == null) {
			simTimer = new Timer();
			try {
				simTask = new SimDataTask(filename, vehicleId, this);
				simTimer.schedule(simTask, 0, updateFrequenzy);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return true;
	}

	/*
	 * An listener can can be used to get notifications when GPS data 
	 * is recived and also what sort of information has been recived. 
	 * See GPSUpdateListener interface for further explenation. 
	 */
	
	public GPSUpdateListener getListener() {
		return updateListener;
	}

	public void setListener(GPSUpdateListener updateListener) {
		this.updateListener = updateListener;
	}

	public synchronized GPSDataGPGGA getGPGGA() {
		return GPGGA;
	}

	public synchronized void setGPGGA(GPSDataGPGGA gpgga) {
		this.latitude = gpgga.getLatitude();
		this.latitudeType = gpgga.getLatitudeType();
		this.longitude = gpgga.getLongitude();
		this.longitudeType = gpgga.getLongitudeType();
		this.setNumberOfSatellites(gpgga.getNumberOfSattelites());
		
		GPGGA = gpgga;
		
		if(updateListener != null) {
			updateListener.GPSUpdateEvent(new GPSUpdateEvent(this, GPSUpdateEvent.LOC_AND_SAT_UPDATE));
		}
	}

	public synchronized GPSDataGPGLL getGPGLL() {
		return GPGLL;
	}

	public synchronized void setGPGLL(GPSDataGPGLL gpgll) {
		this.latitude = gpgll.getLatitude();
		this.latitudeType = gpgll.getLatitudeType();
		this.longitude = gpgll.getLongitude();
		this.longitudeType = gpgll.getLongitudeType();
		
		GPGLL = gpgll;
		
		if(updateListener != null) {
			updateListener.GPSUpdateEvent(new GPSUpdateEvent(this, GPSUpdateEvent.LOCATION_UPDATE));
		}
	}

	public synchronized GPSDataGPGSA getGPGSA() {
		return GPGSA;
	}

	public synchronized void setGPGSA(GPSDataGPGSA gpgsa) {
		GPGSA = gpgsa;
		
		if(updateListener != null) {
			updateListener.GPSUpdateEvent(new GPSUpdateEvent(this, GPSUpdateEvent.SATTELITE_UPDATE));
		}
	}

	public synchronized GPSDataGPRMC getGPRMC() {
		return GPRMC;
	}

	public synchronized void setGPRMC(GPSDataGPRMC gprmc) {
		this.latitude = gprmc.getLatitude();
		this.latitudeType = gprmc.getLatitudeType();
		this.longitude = gprmc.getLongitude();
		this.longitudeType = gprmc.getLongitudeType();
		
		GPRMC = gprmc;
		
		if(updateListener != null) {
			updateListener.GPSUpdateEvent(new GPSUpdateEvent(this, GPSUpdateEvent.LOCATION_UPDATE));
		}
	}

	public synchronized GPSDataGPVTG getGPVTG() {
		return GPVTG;
	}

	public synchronized void setGPVTG(GPSDataGPVTG gpvtg) {
		this.setSpeedKilometer(gpvtg.getGroundSpeedKilometers());
		GPVTG = gpvtg;
		
		if(updateListener != null) {
			updateListener.GPSUpdateEvent(new GPSUpdateEvent(this, GPSUpdateEvent.VELOCITY_UPDATE));
		}
	}

	public synchronized double getLatitude() {
		return latitude;
	}

	public synchronized void setLatitude(double latitude) {
		this.latitude = latitude;
	}

	public synchronized String getLatitudeType() {
		return latitudeType;
	}

	public synchronized void setLatitudeType(String latitudeType) {
		this.latitudeType = latitudeType;
	}

	public synchronized double getLongitude() {
		return longitude;
	}

	public synchronized void setLongitude(double longitude) {
		this.longitude = longitude;
	}

	public synchronized String getLongitudeType() {
		return longitudeType;
	}

	public synchronized void setLongitudeType(String longitudeType) {
		this.longitudeType = longitudeType;
	}
	
	public synchronized double[] getPosition() {
		double pos[] = new double[2];
		pos[0] = this.latitude;
		pos[1] = this.longitude;
		return pos;
	}

	public synchronized double getSpeedKilometer() {
		return speedKilometer;
	}

	public synchronized void setSpeedKilometer(double speedKilometer) {
		this.speedKilometer = speedKilometer;
	}

	public synchronized int getNumberOfSatellites() {
		return numberOfSatellites;
	}

	public synchronized void setNumberOfSatellites(int numberOfSatellites) {
		this.numberOfSatellites = numberOfSatellites;
	}
	
	public double[] convertRT90ToWGS84(double x, double y) {
		double xi = (x - false_northing) / (scale * a_roof);
		double eta = (y - false_easting) / (scale * a_roof);
		double xi_prim = xi - delta1 * Math.sin(2.0 * xi) * Math.cosh(2.0 * eta) - delta2 * Math.sin(4.0 * xi)
						 * Math.cosh(4.0 * eta) - delta3 * Math.sin(6.0 * xi) * Math.cosh(6.0 * eta) - delta4 * Math.sin(8.0 * xi)
						 * Math.cosh(8.0 * eta);
		double eta_prim = eta - delta1 * Math.cos(2.0 * xi) * Math.sinh(2.0 * eta) - delta2 * Math.cos(4.0 * xi)
						  * Math.sinh(4.0 * eta) - delta3 * Math.cos(6.0 * xi) * Math.sinh(6.0 * eta) - delta4 * Math.cos(8.0 * xi)
						  * Math.sinh(8.0 * eta);
		double phi_star = Math.asin(Math.sin(xi_prim) / Math.cosh(eta_prim));
		double delta_lambda = Math.atan(Math.sinh(eta_prim) / Math.cos(xi_prim));
		double lon_radian = lambda_zero + delta_lambda;
		double lat_radian = phi_star + Math.sin(phi_star) * Math.cos(phi_star) * (Astar + Bstar * Math.pow(Math.sin(phi_star), 2) + Cstar
							* Math.pow(Math.sin(phi_star), 4) + Dstar * Math.pow(Math.sin(phi_star), 6));
		
		double[] latlon = new double[2];
		
		latlon[0] = lat_radian * 180.0 / Math.PI;
		latlon[1] = lon_radian * 180.0 / Math.PI;

		return latlon;
	}
	
	
}
